Skip to content

feat(oauth): log rate-limit rejections on the OAuth endpoints as security events - #1174

Merged
mforce merged 2 commits into
mainfrom
feat/1164-oauth-rate-limit-events
Oct 10, 2026
Merged

mforce merged 2 commits into
mainfrom
feat/1164-oauth-rate-limit-events

Conversation

@mforce

@mforce mforce commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

The global rate-limit rejection callback logged Auth.RateLimitRejected only for the login and refresh policies. A 429 from oauth-authorize, oauth-token, oauth-register or oauth-api produced no security event.

Closes #1164

What changed

CluckworkRateLimitingServiceCollectionExtensions still reads the policy attached to the endpoint. It now logs the event for every policy except client-errors, so the four OAuth policies are covered. A policy added later also logs unless someone excludes it. The event gains a Policy property:

{SecurityEvent} policy={Policy} client={ClientIp} path={Path}

The 429 body, Retry-After, the limits and the partition keys did not change. Path is Request.Path, which never includes the query string. The event logs the client IP, not the oauth-api partition key, because that key is a hash of the bearer token.

I chose an exclusion over an allow-list of six names. Both earlier misses had the same cause: a list that did not name the new endpoints. Only client-errors is meant to stay silent, so it is the only name the code checks.

The comments in SecurityEvents and DistributedFixedWindowPolicy, and the event table in docs/security/log-redaction-policy.md, now list the six policies and the Policy field.

Tests

AuthRateLimitLoggingTests gains one theory with four cases, one per OAuth policy. The factory sets each OAuth limit to 1. oauth-api uses a test-only AcceptOAuthTokens endpoint, because no production endpoint accepts OAuth tokens yet. Each case checks that:

  • the first request is allowed and logs no event;
  • the second request returns 429 with Retry-After and logs exactly one warning with the expected Policy, ClientIp and Path;
  • a secret sent in the query string, the form body, the JSON body and the bearer never appears in the rendered event.

The existing client-errors control still asserts no event. The login and step-up tests still pass.

Local runs at 9aca684c, with Testcontainers Postgres:

  • AuthRateLimitLoggingTests, DistributedRateLimiterWiringTests, OAuthFailClosedTests and OAuthClientRegistrationTests: 120 passed, 0 failed.
  • The ImagePin tracked-file guards: 2 passed.

No test or guard registry lists security events or policy names. A grep of tests/ for SecurityEvents and the policy-name constants found only the two rate-limit test files above.

Mutation check

I ran each mutation against the callback at 9aca684c, then ran AuthRateLimitLoggingTests (7 tests) and restored the file.

Mutation Red Green
Restore the old login/refresh allow-list all 4 OAuth cases client-errors, login, step-up
Exclude oauth-api as well as client-errors oauth-api only the other 6
Drop the client-errors exclusion the client-errors control only the other 6
Append Request.QueryString to Path oauth-authorize only (its secret travels in the query string) the other 6

…rity events

The global OnRejected callback emitted Auth.RateLimitRejected only for the
login and refresh policies. It now emits for every attached policy except
client-errors, which adds oauth-authorize, oauth-token, oauth-register and
oauth-api, and the event carries the policy name.

Refs #1164
@mforce
mforce merged commit 9f7efd4 into main Oct 10, 2026
19 checks passed
@mforce
mforce deleted the feat/1164-oauth-rate-limit-events branch October 10, 2026 05:53
@mforce mforce mentioned this pull request Oct 10, 2026
16 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Emit RateLimitRejected security events for all four OAuth rate-limit policies

1 participant